// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.scene;

import bats.BatsPrinter;
import bats.SpatialProperties;
import bats.map.Blocks;
import bats.map.MapNode;
import bats.unit.Targets;
import bats.unit.UnitClass;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.collision.UnsupportedCollisionException;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import java.util.Collection;
import java.util.TreeSet;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.MyString;
import jme3utilities.debug.SkeletonDebugControl;

/**
 * A node control to manage the pickable spatials in a scene. In simulation
 * mode, the pickables consist of all unimpaired units. In build mode, the
 * pickables consist of all props and map blocks.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class PickablesNode
        extends NodeControl {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(PickablesNode.class.getName());
    /**
     * name for the node in the scene graph
     */
    final private static String nodeName = "pickables";
    // *************************************************************************
    // constructors

    /**
     * Instantiate the enabled control for a specified scene. After
     * instantiation, the first method invoked should be initialize().
     *
     * @param scene where these pickables reside (not null)
     */
    PickablesNode(SceneNode scene) {
        super(scene);
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Add a spatial to the pickable set.
     *
     * @param spatial which spatial to add (not null)
     */
    public void add(Spatial spatial) {
        assert spatial != null;
        assert SpatialProperties.hasPickType(spatial);

        attach(spatial);
    }

    /**
     * Clear the pick. In other words, unpick all picked spatials.
     */
    public void clearPick() {
        for (Spatial child : getChildren()) {
            SpatialProperties.setPicked(child, false);
        }
    }

    /**
     * Run a geometric collision test on the pickable set.
     *
     * @return a new list of collisions
     */
    public CollisionResults collideWith(Collidable other)
            throws UnsupportedCollisionException {
        CollisionResults results = new CollisionResults();
        getNode().collideWith(other, results);

        return results;
    }

    /**
     * Count how many spatials are currently picked.
     *
     * @return count (&ge;0)
     */
    public int countPicked() {
        int result = 0;

        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                result++;
            }
        }

        return result;
    }

    /**
     * Get a textual description of the current pick.
     *
     * @return description string
     */
    public String describePick() {
        int count = countPicked();
        if (count == 0) {
            return "none";
        }
        Spatial first = findFirstPicked();
        String name = first.getName();
        String quoted = MyString.quote(name);
        if (count == 1) {
            return quoted;
        }

        return String.format("%s +%d", quoted, count - 1);
    }

    /**
     * Get the world coordinates of a pickable's indicator.
     *
     * @param spatial which pickable to locate (not null)
     * @return a new vector (not null)
     */
    public Vector3f getIndicatorLocation(Spatial spatial) {
        assert spatial != null;

        Vector3f result;
        if (SpatialProperties.isBlock(spatial)) {
            result = Blocks.getIndicatorLocation(spatial);
        } else if (SpatialProperties.isProp(spatial)) {
            result = Props.getIndicatorLocation(spatial);
        } else {
            assert SpatialProperties.isUnit(spatial) : spatial;
            UnitClass unitClass = scene.getWorld().getUnitClass(spatial);
            result = unitClass.getIndicatorLocation(spatial);
        }
        return result;
    }

    /**
     * Enumerate all targets of all pickables.
     *
     * @return a new array of targets which reference existing spatials
     */
    public Targets getTargets() {
        Targets result = new Targets(scene);
        for (Spatial child : getChildren()) {
            UnitClass unitClass = scene.getWorld().getUnitClass(child);
            for (Vector3f location : unitClass.getTargets(child)) {
                result.add(location, child);
            }
        }
        return result;
    }

    /**
     * Initialize this control.
     */
    public void initialize() {
        assert spatial == null : spatial;

        Node node = new Node(nodeName);
        node.addControl(this);
        /*
         * Attach to the scene graph as a child of the world node.
         */
        scene.getWorld().attach(node);
        assert spatial != null;
    }

    /**
     * Make all blocks and props unpickable.
     */
    public void makeUnpickable() {
        MapNode map = scene.getWorld().getMap();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isBlock(child)) {
                SpatialProperties.setPicked(child, false);
                map.attach(child);
            } else if (SpatialProperties.isProp(child)) {
                SpatialProperties.setPicked(child, false);
                scene.getWorld().attach(child);
            }
        }
    }

    /**
     * Process an action which has just begun.
     *
     * @param words action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    public boolean processAction(String[] words) {
        String verb = words[0];
        switch (verb) {
            case "delete":
                if (words.length == 2 && words[1].equals("picked")) {
                    deletePicked();
                    return true;
                }
                break;

            case "duplicate":
                if (words.length == 2 && words[1].equals("picked")) {
                    duplicatePicked();
                    return true;
                }
                break;

            case "impair":
                if (words.length == 1) {
                    // Impair the picked spatials.
                    impairPicked();
                    return true;
                }
                break;

            case "pick":
                if (words.length == 2) {
                    switch (words[1]) {
                        case "all":
                            pickAll();
                            return true;
                        case "invert":
                            invertPick();
                            return true;
                        case "none":
                            clearPick();
                            return true;
                        case "similar":
                            pickSimilar();
                            return true;
                    }
                }
                break;

            case "print":
                if (words.length == 2) {
                    if ("pickables".equals(words[1])) {
                        new BatsPrinter().printSubtree(spatial);
                        return true;
                    }
                }
                break;

            case "reload":
                if (words.length == 2) {
                    if ("picked".equals(words[1])) {
                        reloadPicked();
                        return true;
                    }
                }
                break;

            case "rotate":
                if (words.length == 2) {
                    switch (words[1]) {
                        case "left":
                            rotatePicked(-FastMath.HALF_PI);
                            return true;
                        case "right":
                            rotatePicked(FastMath.HALF_PI);
                            return true;
                    }
                }
                break;

            case "step":
                if (words.length == 2) {
                    switch (words[1]) {
                        case "back":
                            stepPicked(-1f, 0f, 0f);
                            return true;
                        case "down":
                            stepPicked(0f, 0f, -1f);
                            return true;
                        case "forward":
                            stepPicked(1f, 0f, 0f);
                            return true;
                        case "left":
                            stepPicked(0f, -1f, 0f);
                            return true;
                        case "right":
                            stepPicked(0f, 1f, 0f);
                            return true;
                        case "up":
                            stepPicked(0f, 0f, 1f);
                            return true;
                        case "viewBottom":
                            stepViewPicked(-1f, 0f);
                            return true;
                        case "viewLeft":
                            stepViewPicked(0f, -1f);
                            return true;
                        case "viewRight":
                            stepViewPicked(0f, 1f);
                            return true;
                        case "viewTop":
                            stepViewPicked(1f, 0f);
                            return true;
                    }
                }
                break;

            case "toggle":
                if (words.length > 1) {
                    switch (words[1]) {
                        case "aggressive":
                            if (words.length == 2) {
                                /*
                                 * Toggle aggressive behavior in picked units.
                                 */
                                toggleAggresivePicked();
                                return true;
                            }
                            break;
                        case "skeleton":
                            /*
                             * Toggle skeleton debug rendering.
                             */
                            if (words.length == 3 && words[2].equals("all")) {
                                toggleSkeletonDebugAll();
                                return true;
                            } else if (words.length == 2) {
                                toggleSkeletonDebugPicked();
                                return true;
                            }
                    }
                }
        }

        return false; // not handled
    }

    /**
     * Visit all pickables.
     *
     * @param visitor method invoked on each spatial (not null)
     */
    public void visitAll(SceneGraphVisitor visitor) {
        for (Spatial child : getChildren()) {
            visitor.visit(child);
        }
    }
    // *************************************************************************
    // private methods

    /**
     * Delete all picked spatials.
     */
    private void deletePicked() {
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                scene.deleteSpatial(child);
            }
        }
    }

    /**
     * Duplicate all picked blocks and props.
     */
    private void duplicatePicked() {
        WorldNode world = scene.getWorld();
        Blocks blocks = world.getMap().getBlocks();
        Props props = world.getProps();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                if (SpatialProperties.isBlock(child)) {
                    blocks.duplicate(child);
                } else if (SpatialProperties.isProp(child)) {
                    props.duplicate(child);
                }
            }
        }
    }

    /**
     * Find the first picked spatial.
     *
     * @return the pre-existing spatial (or null if none picked)
     */
    private Spatial findFirstPicked() {
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                return child;
            }
        }
        return null;
    }

    /**
     * Impair all picked units.
     */
    private void impairPicked() {
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                UnitClass units = scene.getWorld().getUnitClass(child);
                units.impair(child);
            }
        }
    }

    /**
     * Invert the pick. In other words, pick all unpicked pickables and unpick
     * all picked ones.
     */
    private void invertPick() {
        for (Spatial child : getChildren()) {
            SpatialProperties.togglePick(child);
        }
    }

    /**
     * Maximize the pick. In other words, pick all unpicked spatials.
     */
    private void pickAll() {
        for (Spatial child : getChildren()) {
            SpatialProperties.setPicked(child, true);
        }
    }

    /**
     * Pick every pickable which is similar to one already picked.
     */
    private void pickSimilar() {
        Collection<String> types = new TreeSet<>();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                String prefixType = SpatialProperties.getPrefixType(child);
                types.add(prefixType);
            }
        }
        for (Spatial child : getChildren()) {
            String prefixType = SpatialProperties.getPrefixType(child);
            if (types.contains(prefixType)) {
                SpatialProperties.setPicked(child, true);
            }
        }
    }

    /**
     * Reload each of the picked blocks and props.
     */
    private void reloadPicked() {
        WorldNode world = scene.getWorld();
        Blocks blocks = world.getMap().getBlocks();
        Props props = world.getProps();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                if (SpatialProperties.isBlock(child)) {
                    blocks.reload(child);
                } else if (SpatialProperties.isProp(child)) {
                    props.reload(child);
                }
            }
        }
    }

    /**
     * Rotate each of the picked blocks around its Y-axis.
     *
     * @param angle rotation angle (clockwise, in radians)
     */
    private void rotatePicked(float angle) {
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                MySpatial.rotateY(child, angle);
            }
        }
    }

    /**
     * Move each of the picked blocks a certain number of grid units.
     *
     * @param forward grid units forward
     * @param right grid units rightward
     * @param up grid units upward
     */
    private void stepPicked(float forward, float right, float up) {
        Blocks blocks = scene.getWorld().getMap().getBlocks();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                blocks.moveLocal(child, forward, right, up);
            }
        }
    }

    /**
     * Move each of the picked blocks a certain number of grid units relative to
     * the view.
     *
     * @param up grid units upward in the view
     * @param right grid units rightward in the view
     */
    private void stepViewPicked(float up, float right) {
        Blocks blocks = scene.getWorld().getMap().getBlocks();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                blocks.moveView(child, up, right);
            }
        }
    }

    /**
     * Toggle the aggressive state of all picked nodes.
     */
    private void toggleAggresivePicked() {
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                SpatialProperties.toggleAggressive(child);
            }
        }
    }

    /**
     * Toggle the skeleton debug state of all pickable nodes.
     */
    private void toggleSkeletonDebugAll() {
        for (Spatial child : getChildren()) {
            SkeletonDebugControl debug =
                    child.getControl(SkeletonDebugControl.class);
            debug.toggleEnabled();
        }
    }

    /**
     * Toggle the skeleton debug state of all picked nodes.
     */
    private void toggleSkeletonDebugPicked() {
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isPicked(child)) {
                SkeletonDebugControl debug =
                        child.getControl(SkeletonDebugControl.class);
                debug.toggleEnabled();
            }
        }
    }
}